﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Gemli.Collections;
using System.Reflection;
using System.Data;
using Gemli.Reflection;

namespace Gemli.Data
{
    /// <summary>
    /// Describes the CLR-to-database mapping metadata
    /// associated with a particular CLR type. This
    /// has a scope of CLR object to database table.
    /// </summary>
    public class DataModelMap
    {
        private bool ForceFieldInferencesIfNone;

        /// <summary>
        /// Constructs the mapping metadata using the specified type
        /// by introspecting its attributes.
        /// </summary>
        /// <seealso cref="DataModelTableMappingAttribute"/>
        /// <seealso cref="DataModelFieldMappingAttribute"/>
        /// <seealso cref="DataModelForeignKeyAttribute"/>
        /// <param name="type"></param>
        public DataModelMap(Type type)
        {
            if (type.IsDataModelWrapper())
            {
                type = type.GetDataModelWrapperGenericTypeArg();
            }
            if (type.IsOrInherits(typeof(IList)) && type.IsGenericType)
            {
                type = type.GetGenericArguments().Last();
            }
            ForceFieldInferencesIfNone = true; 
            FieldMappings = new CaseInsensitiveDictionary<DataModelFieldMappingAttribute>();
            EntityType = type;
            TableMapping = GetTableMapByAttributes(type);
            ForeignModelMappings = new CaseInsensitiveDictionary<ForeignDataModelAttribute>();
            LoadFieldMappingAttributes();
            LoadForeignKeyMappingAttributes();
            LoadForeignDataModelMappingAttributes();
            //todo: LoadMappingXml();
        }

        private Type GetEntityType(Type type)
        {
            if (type.IsDataModelWrapper())
            {
                type = type.GetDataModelWrapperGenericTypeArg();
            }
            if (type.IsOrInherits(typeof(IList)) && type.IsGenericType)
            {
                type = type.GetGenericArguments().Last();
            }
            return type;
        }

        private bool _FieldMapBehaviorsExecuted;

        private void ExecuteFieldMapBehaviors()
        {
            // Auto-infer a dictionary entry placeholder for foreign entity mappings
            // with no referenced foreign key column
            foreach (var fk in ForeignModelMappings)
            {
                var tmt = GetEntityType(fk.Value.TargetMemberType);
                var ttmap = GetEntityMapping(tmt);
                var ff = ttmap != null
                             ? ttmap.GetFieldMappingByDbColumnName(fk.Value.ForeignColumnName)
                             : null;
                if (FieldMappings.ToList().Exists(p => p
                    .Value.ColumnName.ToLower() == fk.Value.ColumnName.ToLower()))
                {
                    continue;
                }
                var fm = new DataModelFieldMappingAttribute();
                if (ff != null)
                {
                    ff.CopyDeltaTo(fm);
                    fm.TargetMember = fk.Value.TargetMember;
                    fm.TargetMemberType = fk.Value.TargetMemberType;
                    fm.IsPrimaryKey = false;
                    fm.IsIdentity = false;
                    FieldMappings.Add("field:" + fk.Value.ColumnName, fm);
                }
                _FieldMapBehaviorsExecuted = true;
            }

            // Auto-infer IsIdentity if no field is declared as primary key 
            // and no field is declared as identity and one member is named
            // "ID" and that member is an int or a long. 
            bool hasPkOrIdent = false;
            DataModelFieldMappingAttribute idFieldMapping = null;
            string typename = null;
            foreach (var field_kvp in FieldMappings)
            {
                var field = field_kvp.Value;
                if (field.IsIdentityDefined || field.IsPrimaryKeyDefined)
                {
                    hasPkOrIdent = true;
                }
                if (typename == null) typename = field.TargetMember.DeclaringType.Name.ToLower();
                var propname = field.TargetMember.Name.ToLower();
                if (propname == "id" || propname == typename +"id" || propname == typename + "_id")
                {
                    idFieldMapping = field;
                }
            }
            if (!hasPkOrIdent && idFieldMapping != null && 
                (idFieldMapping.DataType == DbType.Int32 ||
                idFieldMapping.DataType == DbType.Int64))
            {
                idFieldMapping.IsIdentity = true;
            } 

            // predetermine NULLables based on CLR nullability
            foreach (var field_kvp in FieldMappings)
            {
                var field = field_kvp.Value;
                if (!field.IsNullableDefined)
                {
                    var memtype = field.TargetMemberType;
                    bool nullable = !memtype.IsValueType;
                    if (memtype.IsGenericType && memtype.GetGenericTypeDefinition() == typeof(Nullable<>)) 
                    {
                        memtype = memtype.GetGenericArguments()[0];
                        nullable = true;
                    }
                    field.IsNullable = (memtype.IsValueType || memtype == typeof(string)) && nullable && !field.IsPrimaryKey;
                }
            }
        }

        private string[] _PrimaryKeyFields;
        /// <summary>
        /// Returns the string array consisting of the 
        /// database primary key columns.
        /// </summary>
        public string[] PrimaryKeyColumns
        {
            get
            {
                if (_PrimaryKeyFields == null)
                {
                    var f = new List<string>();
                    foreach (var field_kvp in this.FieldMappings)
                    {
                        var field = field_kvp.Value;
                        if (field.IsPrimaryKey) f.Add(field.ColumnName);
                    }
                    _PrimaryKeyFields = f.ToArray();
                }
                return _PrimaryKeyFields;
            }
        }

        private Type EntityType { get; set; }

        /// <summary>
        /// Returns a <see cref="DataModelFieldMappingAttribute"/>
        /// for the associated CLR object's mapping
        /// using the specified <paramref name="columnName"/> as the
        /// database column name with which to identify the 
        /// CLR member / DB column field mapping.
        /// </summary>
        /// <param name="columnName"></param>
        /// <returns></returns>
        public DataModelFieldMappingAttribute GetFieldMappingByDbColumnName(string columnName)
        {
            // todo: optimize
            foreach (var fieldMap_kvp in FieldMappings)
            {
                if (fieldMap_kvp.Value.ColumnName.ToLower() == (columnName ?? "").ToLower())
                {
                    return fieldMap_kvp.Value;
                }
            }
            return null;
        }

        /// <summary>
        /// Returns a dictionary of <see cref="DataModelFieldMappingAttribute"/>s
        /// associated with this table / CLR type map. The
        /// <see cref="DataModelFieldMappingAttribute"/>s identify the mappings
        /// between a CLR object's properties/fields and the columns of a database 
        /// table.
        /// </summary>
        public CaseInsensitiveDictionary<DataModelFieldMappingAttribute> FieldMappings { get; private set; }

        /// <summary>
        /// Returns a dictionary of <see cref="ForeignDataModelAttribute"/>s
        /// associated with this table / CLR type map. The
        /// <see cref="ForeignDataModelAttribute"/>s identify the foreign key
        /// relationships between a CLR object's property/field and a foreign
        /// key in a database.
        /// </summary>
        public CaseInsensitiveDictionary<ForeignDataModelAttribute> ForeignModelMappings { get; private set; }

        /// <summary>
        /// Returns the <see cref="DataModelTableMappingAttribute"/> associated with this
        /// table / CLR type map. The <see cref="DataModelTableMappingAttribute"/>
        /// describes, for example, the name of the table and its containing schema.
        /// </summary>
        public DataModelTableMappingAttribute TableMapping { get; private set; }

        private DataModelTableMappingAttribute GetTableMapByAttributes(Type type)
        {
            var hierarchy = new List<Type>();
            var t = type;
            while (t != typeof(DataModel) && t != typeof(object))
            {
                hierarchy.Insert(0, t = t.BaseType);
            }
            hierarchy.Add(type);

            DataModelTableMappingAttribute ret = null;
            // walk up hierarchy
            foreach (var baseType in hierarchy)
            {
                var attrs = baseType.GetCustomAttributes(typeof(DataModelTableMappingAttribute), false);
                if (attrs != null && attrs.Length > 0)
                {
                    if (ret != null &&
                        !((DataModelTableMappingAttribute)attrs[0]).ClearBaseObjectMapping)
                    {
                        ((DataModelTableMappingAttribute)attrs[0]).CopyDeltaTo(ret);
                    }
                    else ret = (DataModelTableMappingAttribute) attrs[0];
                    if (string.IsNullOrEmpty(ret.Table)) ret.Table = type.Name;
                }
            }
            if (ret == null) ret = new DataModelTableMappingAttribute(type.Name);
            return ret;
        }

        private void LoadFieldMappingAttributes()
        {
            var hierarchy = new List<Type>();
            var t = this.EntityType;
            while (t != typeof(DataModel) && t != typeof(object))
            {
                hierarchy.Insert(0, t);
                t = t.BaseType;
            }
            // walk up hierarchy
            foreach (var type in hierarchy)
            {
                var pis = type.GetProperties();
                var fis = type.GetFields();
                var mis = new Dictionary<MemberInfo, Type>();
                foreach (var fi in fis) mis.Add(fi, fi.FieldType); // fee fi fo fum
                foreach (var pi in pis) mis.Add(pi, pi.PropertyType);
                foreach (var mi_kvp in mis)
                {
                    var mi = mi_kvp.Key;
                    var miType = mi_kvp.Value;
                    var attrs = mi.GetCustomAttributes(typeof (DataModelFieldMappingAttribute), false);
                    foreach (DataModelFieldMappingAttribute attr in attrs)
                    {
                        attr.TargetMember = mi;
                        attr.TargetMemberType = miType;
                        if (string.IsNullOrEmpty(attr.ColumnName))
                        {
                            attr.ColumnName = mi.Name;
                            attr.IsColumnNameInferred = true;
                        }
                        if (FieldMappings.ContainsKey(mi.Name) && !attr.ClearBaseObjectMapping)
                        {
                            attr.CopyDeltaTo(FieldMappings[mi.Name]);
                        }
                        else FieldMappings[mi.Name] = attr;
                        var fm = FieldMappings[mi.Name];
                        if (fm.DataType == DbType.Object)
                        {
                            var def = miType;
                            Type[] genargs;
                            if (def.IsGenericType &&
                                (genargs = def.GetGenericArguments()).Length == 1)
                            {
                                def = genargs[0];
                            }
                            try
                            {
                                fm.DataType = TypeConvertor.ToDbType(def);
                            }
                            catch { }
                        }
                    }
                }
            }
            if (this.FieldMappings.Count == 0 && ForceFieldInferencesIfNone)
            {
                foreach (var type in hierarchy)
                {
                    var pis = type.GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);
                    var fis = type.GetFields(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);
                    var mis = new Dictionary<MemberInfo, Type>();
                    foreach (var fi in fis) mis.Add(fi, fi.FieldType); // fee fi fo fum
                    foreach (var pi in pis) mis.Add(pi, pi.PropertyType);
                    foreach (var mi_kvp in mis)
                    {
                        var mi = mi_kvp.Key;
                        var miType = mi_kvp.Value;
                        var attr = new DataModelFieldMappingAttribute(mi.Name);
                        attr.TargetMember = mi;
                        attr.TargetMemberType = miType;
                        FieldMappings[mi.Name] = attr;
                        var fm = attr;
                        if (fm.DataType == DbType.Object)
                        {
                            var def = miType;
                            Type[] genargs;
                            if (def.IsGenericType &&
                                (genargs = def.GetGenericArguments()).Length == 1)
                            {
                                def = genargs[0];
                            }
                            try
                            {
                                fm.DataType = TypeConvertor.ToDbType(def);
                            } catch {}
                        }
                    }
                }
            }
        }

        private void LoadForeignKeyMappingAttributes()
        {
            var hierarchy = new List<Type>();
            var t = this.EntityType;
            while (t != typeof(DataModel) && t != typeof(object))
            {
                hierarchy.Insert(0, t);
                t = t.BaseType;
            }
            // walk up hierarchy
            foreach (var type in hierarchy)
            {
                var pis = type.GetProperties();
                var fis = type.GetFields();
                var mis = new Dictionary<MemberInfo, Type>();
                foreach (var fi in fis) mis.Add(fi, fi.FieldType); // fee fi fo fum
                foreach (var pi in pis) mis.Add(pi, pi.PropertyType);
                foreach (var mi_kvp in mis)
                {
                    var mi = mi_kvp.Key;
                    var miType = mi_kvp.Value;
                    var attrs = mi.GetCustomAttributes(typeof (DataModelForeignKeyAttribute), false);
                    foreach (DataModelForeignKeyAttribute attr in attrs)
                    {
                        attr.TargetMember = mi;
                        attr.TargetMemberType = miType;
                        if (!FieldMappings.ContainsKey(mi.Name))
                        {
                            FieldMappings[mi.Name] 
                                = new DataModelFieldMappingAttribute(attr.OnMatchDataField);
                        }
                        if (FieldMappings[mi.Name].ForeignKeyMapping != null && !attr.ClearBaseObjectMapping)
                        {
                            attr.CopyDeltaTo(FieldMappings[mi.Name].ForeignKeyMapping);
                        }
                        else FieldMappings[mi.Name].ForeignKeyMapping = attr;
                    }
                }
            }
        }

        private void LoadForeignDataModelMappingAttributes()
        {
            var hierarchy = new List<Type>();
            var t = this.EntityType;
            while (t != typeof(DataModel) && t != typeof(object))
            {
                hierarchy.Insert(0, t);
                t = t.BaseType;
            }
            // walk up hierarchy
            foreach (var type in hierarchy)
            {
                var pis = type.GetProperties();
                var fis = type.GetFields();
                var mis = new Dictionary<MemberInfo, Type>();
                foreach (var fi in fis) mis.Add(fi, fi.FieldType); // fee fi fo fum
                foreach (var pi in pis) mis.Add(pi, pi.PropertyType);
                foreach (var mi_kvp in mis)
                {
                    var mi = mi_kvp.Key;
                    var miType = mi_kvp.Value;
                    var attrs = mi.GetCustomAttributes(typeof(ForeignDataModelAttribute), false);
                    foreach (ForeignDataModelAttribute attr in attrs)
                    {
                        attr.TargetMember = mi;
                        attr.TargetMemberType = miType;
                        if (ForeignModelMappings.ContainsKey(mi.Name) && !attr.ClearBaseObjectMapping)
                        {
                            attr.CopyDeltaTo(ForeignModelMappings[mi.Name]);
                        }
                        else ForeignModelMappings[mi.Name] = attr;
                    }
                }
            }
        }

        private void LoadMappingXml()
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// A two-argument generic type 
        /// for generic argument type containment.
        /// </summary>
        /// <remarks>
        /// Used internally to deal with classes that are, for example,
        /// mapped to mapping tables (many-to-many relationships).
        /// <seealso cref="TempMappingTable{TLeft,TRight}"/>
        /// </remarks>
        /// <typeparam name="A"></typeparam>
        /// <typeparam name="B"></typeparam>
        public class MappingType<A,B> { }

        /// <summary>
        /// Returns the <see cref="DataModelMap"/> that is associated
        /// with the specified CLR <paramref name="type"/>. This value
        /// contains all the mapping information needed to bind a CLR
        /// type to a database table and its relationships.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static DataModelMap GetEntityMapping(Type type)
        {
            if (type.IsDataModelWrapper(false))
            {
                type = type.GetDataModelWrapperGenericTypeArg();
            }
            if (!MapItems.ContainsKey(type))
            {
                return GetEntityMapping(type, MapItems);
            }

            return MapItems[type];
        }


        private static readonly Dictionary<Type, DataModelMap> _MapDictionary
            = new Dictionary<Type, DataModelMap>();
        /// <summary>
        /// Returns the static type map dictionary for all
        /// loaded <see cref="DataModelMap"/>s.
        /// </summary>
        public static Dictionary<Type, DataModelMap> MapItems
        {
            get { return _MapDictionary; }
        }

        private static DataModelMap GetEntityMapping(Type type, 
            IDictionary<Type, DataModelMap> dictionary)
        {
            if (dictionary.ContainsKey(type)) return dictionary[type];
            var hierarchy = new List<Type>();
            var t = type;
            while(t != typeof(DataModel) && t != typeof(object))
            {
                hierarchy.Insert(0, t = t.BaseType);
            }
            hierarchy.Add(type);

            // walk up hierarchy
            foreach (var baseType in hierarchy)
            {
                if (!dictionary.ContainsKey(baseType))
                {
                    dictionary.Add(baseType, new DataModelMap(baseType));
                }
            }
            var ret = dictionary[type];
            if (!ret._FieldMapBehaviorsExecuted)
            {
                ret.ExecuteFieldMapBehaviors();
            }
            return ret;
        }

    }
}
